package registnet.enlistment;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Vector;

import org.apache.torque.TorqueException;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.DesktopUnavailableException;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.util.Clients;

import registnet.base.Course;
import registnet.base.CoursePeer;
import registnet.base.Priority;
import registnet.base.Standing;
import registnet.base.courseoffering.CourseOffering;
import registnet.base.student.Student;
import registnet.util.schedule.Schedule;
import resources.Constants;

/**
 * A class that handles the enlistment process.
 */
public class Enlistor extends Thread{
	
	/**
	 * A numerical representation of the progress of the pre-enlistment process.
	 */
	private Double progress = 0d;
	
	/**
	 * A <code>String</code> representation of the total time taken to do the pre-enlistment process.
	 */
	private String totalTimeDisp = "";
	
	/**
	 * The desktop used in the GUI.
	 */
	private final Desktop desktop;
	
	/**
	 * The list of students.
	 */
	private List<Student> students;
	
	/**
	 * A mapping of the course code and the course itself.
	 */
	private Map<String, Course> courses;
	
	/**
	 * Default constructor.
	 */
	public Enlistor(Desktop desktop){
		students = new Vector<Student>();
		courses = new HashMap<String, Course>();
		progress = 0d;
		totalTimeDisp = "";
		this.desktop = desktop;
	}

	@Override
	public void run(){
		try{
			doEnlistment();
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			desktop.enableServerPush(false);
		}
		return;
	}
	
	/**
	 * Do the enlistment process.
	 * @throws InterruptedException 
	 * @throws DesktopUnavailableException 
	 */
	public void doEnlistment() throws DesktopUnavailableException, InterruptedException{
		Integer semester = (new registnet.dao.SystemPropertiesPeer().getSystemProperties().getSemester() + 1)%3;
		Integer year = new registnet.dao.SystemPropertiesPeer().getSystemProperties().getAcademicYear();
		Date initialDate;
		Date finalDate;
		if(semester==3) year++;
		progress = 0d;
		Executions.activate(desktop);
		Clients.showBusy("Please wait. Executing Step 1 of 4.", true);
		Executions.deactivate(desktop);
		
		/*
		 * Prepares the database
		 */
		new registnet.dao.CourseOfferingSlotPeer().deleteAllCourseOfferingSlot();
		new registnet.dao.SlotPeer().deleteAllSlots();
		
		initialDate = new Date();
		
		/*
		 * Rank courses
		 */
		CoursePeer coursePeer = new CoursePeer();
		coursePeer.sortCoursesByClassHours(); 
		coursePeer.rankCourses();
		coursePeer.sortCoursesByRank(); 
		for(Course course : coursePeer.getCourses())
			courses.put(course.getCourseCode(), course);
		Executions.activate(desktop);
		Clients.showBusy("Please wait. Executing Step 2 of 4.", true);
		Executions.deactivate(desktop);
		
		/*
		 * Load list of students
		 */
		for(registnet.dao.Student daoStudent : new registnet.dao.StudentPeer().getAllActiveStudents()){
			Student student = new Student(daoStudent.getStudentNumber());
			student.setStudentNumber(daoStudent.getStudentNumber());
			
			//	Load student priority
			try {
				student.setPriority(new Priority(new registnet.dao.StudentPriorityPeer().getStudentPriority(daoStudent.getStudentId()).getLevel()));
			}catch (Exception e1) { System.err.println("****[error] Student priority loading failed. "+e1.getMessage()); continue;}
			
			//	Load student standing
			int standing = new registnet.dao.StudentStandingPeer().getStudentStanding(daoStudent.getStudentId()).getStanding();
			if(Constants.MAP_STANDING_KEY.get(standing).equals(Constants.STANDING_GOOD_STANDING))
				student.setStanding(Standing.GOOD_STANDING);
			else if(Constants.MAP_STANDING_KEY.get(standing).equals(Constants.STANDING_WARNING))
				student.setStanding(Standing.WARNING);
			else if(Constants.MAP_STANDING_KEY.get(standing).equals(Constants.STANDING_PROBATION))
				student.setStanding(Standing.PROBATION);
			else if(Constants.MAP_STANDING_KEY.get(standing).equals(Constants.STANDING_DISMISSED))
				student.setStanding(Standing.DISMISSED);
			else if(Constants.MAP_STANDING_KEY.get(standing).equals(Constants.STANDING_PERMANENTLY_DISQUALIFIED))
				student.setStanding(Standing.PERMANENTLY_DISQUALIFIED);
			else{
				System.err.println("****[error] Student standing loading failed.");
				continue;
			}
			
			//	Load list of recommended courses of each student
			for(registnet.dao.StudentRecommendedCourse daoSRC : new registnet.dao.StudentRecommendedCoursePeer().getRecommendedCoursesByStudent(daoStudent.getStudentId())){
				String courseCode = "";
				try {
					courseCode = daoSRC.getCourse().getCourseCode();
					student.addRecommendedCourse(courses.get(courseCode));
				} catch (TorqueException e) { System.err.println("****[error] Student recommended courses loading failed. "+e.getMessage()); }
			}
			
			//	Initialize schedule of student
			student.setSchedule(new Schedule());
			
			students.add(student);
		}
		Executions.activate(desktop);
		Clients.showBusy("Please wait. Executing Step 3 of 4.", true);
		Executions.deactivate(desktop);
		
		/*
		 * Main Processing: do processing to each course
		 */
		for(Course course : courses.values()){
			List<Student> studentList = new Vector<Student>();
			List<CourseOffering> courseOfferingList = new Vector<CourseOffering>();
			Integer courseId = new registnet.dao.CoursePeer().getCourseId(course.getCourseCode());
			
			/*
			 * Make list of students taking the course
			 */
			for(Student student : students){
				if(student.hasRecommendedCourse(course))
					studentList.add(student);
			}
			
			/*
			 * Make list of section of the course
			 */
			courseOfferingList = course.getCourseOfferings();
			
			/*
			 * Determine option of students(sections that are not in conflict with student's schedule)
			 * and rank options according to "desirability"
			 */
			for(Student student : studentList){
				//	determine options
				student.setOptions(new Vector<CourseOffering>());	//initialize options
				for(CourseOffering courseOffering : courseOfferingList){
					if(!student.getSchedule().isConflict(courseOffering.getSchedule())){
						student.addOption(courseOffering);
					}
				}
				//	rank options
				//student.rankOptions();		//1st implementation
				student.rankOptions(course.getCourseCode());	//2nd implementation
				student.sortOptionsByRank();
			}
			
			/*
			 * Sort students according to the number of options and priority
			 */
			sortStudentsByNumberOfOptions(studentList);
			sortStudentsByPriority(studentList);
			
			/*
			 * Assign each student to his most desirable section
			 */
			List<Student> studentListTemp = studentList;
			List<Student> studentListTemp1 = new Vector<Student>();
			int size = studentListTemp.size();
			while(size!=0){
				int reRankFlag = 0;
				studentListTemp1 = new Vector<Student>();
				for(Student student : studentListTemp){
					if(reRankFlag == 1){
						studentListTemp1.add(student);
						continue;
					}
					/*
					 * get best option
					 */
					if(student.getOptions().size()==0){
						size--;
						continue;
					}	
					CourseOffering courseOffering = student.getOptions().get(0);
					/*
					 * enlist into the best option
					 */
					int result = courseOffering.enlistStudent(student.getStudentNumber());
					if(result==0){
						reRankFlag = 1;
						studentList = removeOption(studentList, courseOffering);
						student.updateSchedule(courseOffering);
						student.addEnlistedCourseOfferings(courseOffering);
						student.removeRecommendedCourse(course);
						size--;
					}else if(result>0){
						student.updateSchedule(courseOffering);
						student.addEnlistedCourseOfferings(courseOffering);
						student.removeRecommendedCourse(course);
						size--;
					}
				}// end of foreach
				if(reRankFlag == 1){
					sortStudentsByNumberOfOptions(studentListTemp1);
					sortStudentsByPriority(studentListTemp1);
					studentListTemp = studentListTemp1;
					size = studentListTemp.size();
				}
			}
			
			
			/*
			 * Update database
			 */
			for(CourseOffering courseOffering : courseOfferingList){
				Integer courseOfferingId = 0;
				try{
					courseOfferingId = new registnet.dao.CourseOfferingPeer().getCourseOfferingId(courseId, courseOffering.getName(), semester, year);
				}catch(Exception e){ continue; }
				
				for(String studentNumber : courseOffering.getEnlistedStudents()){
					//slot
					try{
						new registnet.dao.SlotPeer().createSlot(new registnet.dao.StudentPeer().getStudentId(studentNumber), courseOfferingId, Constants.MAP_SLOT_STATUS_VALUE.get(Constants.SLOT_STATUS_RESERVED));
					}catch(Exception e){ System.err.println("****[error] Slot database update failed. "+e.getMessage()); }
				}
				//course_offering_slot
				try{
					new registnet.dao.CourseOfferingSlotPeer().createNewCourseOfferingSlot(courseOfferingId, courseOffering.getSlots());
				}catch(Exception e){ System.err.println("****[error] CourseOfferingSlot database creation failed. "+e.getMessage()); }
			}
		}//	end of main processing
		Executions.activate(desktop);
		Clients.showBusy("Please wait. Executing Step 4 of 4.", true);
		Executions.deactivate(desktop);
		
		/*
		 * Post-processing
		 */
		//	TODO	Post-processing can be added
		
		System.out.println("****[check] Enlistment success.");
		finalDate = new Date();
		long totalTime = finalDate.getTime()-initialDate.getTime();
		System.out.println("milliseconds: "+(totalTime%1000));
		System.out.println("seconds: "+(int)((totalTime/1000)%60));
		System.out.println("minutes: "+(int)((totalTime/60000)%60));
		System.out.println("hours: "+(int)((totalTime/3600000)%60));
		
		totalTimeDisp = ""+(int)((totalTime/3600000)%60)+"H:"+(int)((totalTime/60000)%60)+"M:"+(int)((totalTime/1000)%60)+"S";
		
		Executions.activate(desktop);
		Clients.showBusy(null, false);
		Executions.deactivate(desktop);
	}
	
	/**
	 * Compare students based on its option count.
	 * @param student0
	 * @param student1
	 * @return 1 if <code>student0</code>'s option count is greater
	 * than <code>student1</code>'s option count; -1 if <code>student0</code>'s 
	 * option count is less than <code>student1</code>'s option count; and
	 * 0 if both student have equal option count. 
	 */
	private int compareStudentsByOptionCount(Student student0, Student student1){
		if(student0.getOptions().size() > student1.getOptions().size())
			return 1;
		else if(student0.getOptions().size() == student1.getOptions().size())
			return 0;
		else
			return -1;
	}
	
	/**
	 * Compare students based on its priority.
	 * @param student0
	 * @param student1
	 * @return 1 if <code>student0</code>'s priority is greater
	 * than <code>student1</code>'s priority; -1 if <code>student0</code>'s 
	 * priority is less than <code>student1</code>'s priority; and
	 * 0 if both student have equal priority. 
	 */
	private int compareStudentsByPriority(Student student0, Student student1){
		if(student0.getPriority().getDescription() > student1.getPriority().getDescription())
			return 1;
		else if(student0.getOptions().size() == student1.getOptions().size())
			return 0;
		else
			return -1;
	}
	
	/**
	 * Sort students according to its priority.
	 * @param studentList
	 * @return the sorted list
	 */
	private List<Student> sortStudentsByPriority(List<Student> studentList){
		List<Student> students = studentList;
		int n = students.size();
		for(int i=0; i<n-1; i++){
			for(int j=0; j<(n-1-i); j++){
				Student a = students.get(j);
				Student b = students.get(j+1);
				if(compareStudentsByPriority(a,b)>0){
					students.set(j,b);
					students.set(j+1,a);
				}
			}
		}
		return students;
	}
	
	/**
	 * Sort students according to their option count
	 * @param studentList
	 * @return the sorted list.
	 */
	private List<Student> sortStudentsByNumberOfOptions(List<Student> studentList){
		List<Student> students = studentList;
		int n = students.size();
		for(int i=0; i<n-1; i++){
			for(int j=0; j<(n-1-i); j++){
				Student a = students.get(j);
				Student b = students.get(j+1);
				if(compareStudentsByOptionCount(a,b)>0){
					students.set(j,b);
					students.set(j+1,a);
				}
			}
		}
		return students;
	}
	
	/**
	 * Remove from all the students in the given list, the given course offering, from
	 * their list of option course offerings.
	 * 
	 * @param studentList
	 * @param courseOffering
	 * @return the list of students
	 */
	private List<Student> removeOption(List<Student> studentList, CourseOffering courseOffering){
		List<Student> students = studentList;
		ListIterator<Student> li = students.listIterator();
		while(li.hasNext()){
			Student tmpStudent = li.next();
			ListIterator<CourseOffering> liC = tmpStudent.getOptions().listIterator();
			while(liC.hasNext()){
				int index = liC.nextIndex();
				if(liC.next().getName().equals(courseOffering.getName())){
					tmpStudent.getOptions().remove(index);
					break;
				}
			}
		}
		return students;
	}
	
	/**
	 * Returns the progress of the pre-enlistment process.
	 * @return the progress of the pre-enlistment process
	 */
	public Double getProgress() {
		return progress;
	}

	/**
	 * Returns the total time taken by the pre-enlistment process.
	 * @return the total time taken by the pre-enlistment process
	 */
	public String getTotalTimeDisp() {
		return totalTimeDisp;
	}
	
}
